关于“Z Offset”

Posted by FlowingCrescent on 2022-08-15
Estimated Reading Time 3 Minutes
Words 994 In Total
Viewed Times

卡通渲染中有不少trick,个人认为其中最涉及对管线整体了解的一个trick,便是“让眉毛显示在头发前”,看似简单的一个小trick,其实潜藏着许多坑。
image.png

方法一 模板测试

通常这种反直觉的Trick,第一反应便是模板测试,给头发一个模板值A,然后眉毛画两次,一次是正常绘制,一次是开启ZTest Always后,只使用模板测试,当模板值为A时才通过。
这样确实能够实现让眉毛显示在头发上的目标,但是这也会导致一个比较诡异的bug,
——其他人的头发能够显示在另一个人的头发上

因此此方法只能在只有一个角色的时候使用。
说白了GPU就没法判断,哪个片元是谁的头发,应当显示在哪个片元之上。除非我们又给不同角色设定不同的模板值……那又极其蛋疼了。
因此应当还是要借助深度测试。

方法二 Shader API中的Offset

对应openGL中的glPolygonOffset,
在shader中大概写成这样:Offset -100, -200,前一个数字为"Factor",后一个为"Units",根据官方文档,Factor将创建一个偏移的基础值,而Units将会乘以某个特定值后结合Factor进行偏移。
其实就是调整渲染的片元的深度值,但是这个API的实现是"implementation-specific"的,也就是不同GPU会有不同的实现,因此最终结果是难以预测的
同时它还会破坏Early-Z,因为这是对片元做处理,那么没进行处理前谁都不知道它会不会通过ZTest。通常而言这个API只能用来处理Z-Fighting。
因此这个方法也不推荐。

方法三 用算法实现Offset

最后选择用算法去修改深度值,但此时仍然还有两个选择:在片元做处理还是在顶点做处理?
在片元做:
使用SV_DEPTH的语义绑定,我们可以获得并直接修改该片元的output深度值。
但这同样会破坏earlyZ。

The cost incurred by SV_Depth varies depending on the GPU architecture, but overall it’s fairly similar to the cost of alpha testing (using the built-in clip() function in HLSL). Render shaders that modify depth after all regular opaque shaders.

因此最终就是需要在顶点阶段便实现深度偏移。
我们的目标是给出某个精确值,比如0.05,那么片元的线性深度就是减少0.05。
那么其实我们就是要修改顶点所输出的裁剪空间坐标,也就是PosCS。

以下是摘自《Shader入门精要》的顶点投影变换。

输出之后还没有进行透视除法,也就是还未转换至NDC空间,而由于最终显示在屏幕上的位置将由PosCS.x/PosCS.w以及PosCS.y/PosCS.w所决定,因此我们并不能动最终输出的xyw三个值,只能修改z。
我们在Vertex Shader中可以获取投影矩阵,便取得了所有需要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
float4 NiloGetNewClipPosWithZOffset(float4 originalPositionCS, float viewSpaceZOffsetAmount)
{
if(unity_OrthoParams.w == 0)
{
////////////////////////////////
//Perspective camera case
////////////////////////////////
float2 ProjM_ZRow_ZW = UNITY_MATRIX_P[2].zw;
float modifiedPositionVS_Z = -originalPositionCS.w + -viewSpaceZOffsetAmount; // push imaginary vertex
float modifiedPositionCS_Z = modifiedPositionVS_Z * ProjM_ZRow_ZW[0] + ProjM_ZRow_ZW[1];
originalPositionCS.z = modifiedPositionCS_Z * originalPositionCS.w / (-modifiedPositionVS_Z); // overwrite positionCS.z
return originalPositionCS;
}
else
{
////////////////////////////////
//Orthographic camera case
////////////////////////////////
originalPositionCS.z += -viewSpaceZOffsetAmount / _ProjectionParams.z; // push imaginary vertex and overwrite positionCS.z
return originalPositionCS;
}
}

出自:https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample/blob/master/NiloZOffset.hlsl

也就是取用PosCS.w当PosVS.z,然后用投影矩阵的值强行算个修改后的PosCS.z,并抵消掉透视除法。
这种深度Offset方法也可以用于消除脸上某些角度不想要的描边,其实是在卡通渲染中很泛用的一个算法,在卡渲始祖级游戏罪恶装备中已经有应用了:
image.png

参考资料:
https://forum.unity.com/threads/offset-parameters.23281/
https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample/blob/master/NiloZOffset.hlsl
https://www.cnblogs.com/TracePlus/p/4205834.html


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。